package com.huatai.model.service;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.huatai.common.config.MinioAPI;
import com.huatai.common.error.exception.ServiceException;
import com.huatai.common.util.DateTimeUtil;
import com.huatai.model.config.CustomMinioClient;
import com.huatai.model.config.MinioConfig;
import com.huatai.model.constant.MinioBucket;
import com.huatai.model.dto.FileInfoDTO;
import com.huatai.model.dto.UploadPartDTO;
import com.huatai.model.entity.UploadTaskEntity;
import com.huatai.model.mapper.UploadTaskMapper;
import com.huatai.model.vo.FileUploadTaskVO;
import io.minio.ListPartsResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * @author xuzy
 */
@Service
@Slf4j
public class FileService{

	@Autowired
	private UploadTaskMapper mapper;

	@Autowired
	private CustomMinioClient customMinioClient;

	@Autowired
	private MinioAPI minioAPI;

	@Autowired
	MinioConfig minioConfig;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	AsyncService asyncService;

	private final static String SLASH = "/";
	private final static String FILE_NAME_PREFIX_SEPARATOR = "-";

	@Value("${privateBucket.fileType}")
	List<String> privateBucketNameFileSufix;

	private final static long FILE_DOWNLOAD_ZISE = 1024 * 1024 * 10;

	private final static String PRE="file-part-task-";

	/**
	 * 分片上传创建上传任务
	 */
	public FileUploadTaskVO createUploadTask(FileInfoDTO dto) {
		UploadTaskEntity task = getOneUploadTask(dto.getFileIdentifier());

		FileUploadTaskVO vo = new FileUploadTaskVO();
		vo.setListPart(new ArrayList<>());
		String status = "0";
		if (task == null) {
			// 没有上传任务
			String objectKey = getObjectKey(dto.getTotalSize(),dto.getFileName());

			Multimap<String, String> headers = null;
			if (StringUtils.hasText(dto.getFileType())) {
				headers = HashMultimap.create();
				headers.put("Content-Type", dto.getFileType());
			}

			String bucketName = getBucketName(dto.getFileName());
			// 生成 uploadId
			String uploadId = customMinioClient.uploadId(bucketName, objectKey,headers);

			task = UploadTaskEntity.builder()
				.uploadId(uploadId)
				.fileIdentifier(dto.getFileIdentifier())
				.fileName(dto.getFileName())
				.fileType(dto.getFileType())
				.bucketName(bucketName)
				.objectKey(objectKey)
				//.saveFullPath(minioConfig.getENDPOINT() + "/" + bucketName + "/" + objectKey)
				.totalSize(dto.getTotalSize())
				.chunkSize(dto.getChunkSize())
				.chunkNumber(
					BigDecimal.valueOf(dto.getTotalSize())
						.divide(BigDecimal.valueOf(dto.getChunkSize()), RoundingMode.UP)
						.intValue()
				)
				.completed(false)
				.createTime(LocalDateTime.now())
				.fileSufix(dto.getFileName().substring(dto.getFileName().lastIndexOf(".")+1)) .build();
			mapper.insert(task);
		} else {
			//判断任务是否已完成
			if(task.isCompleted()){
				status = "1";
				vo.setFilePath(task.getObjectKey());
			}else{//未完成，判断是否是部分完成，部分完成则返回已上传分片
				try {
					List<Integer> list1 = new ArrayList<>();
					CompletableFuture<ListPartsResponse> future = customMinioClient.listPartsAsync(task.getBucketName(),task.getObjectKey(), null, task.getUploadId());
					List<Integer> list= future.get().result().partList().stream().map(part -> new Integer(part.partNumber())).collect(Collectors.toList());
					if(null != list && list.size()> 0){
						status = "2";
						Integer num = task.getChunkNumber();
						for (int i = 1; i <= num; i++) {
							list1.add(new Integer(i));
						}
						list1.removeAll(list);

						vo.setListPart(list1);
					}
				}catch (Exception e){}

			}
		}
		vo.setStatus(status);
		return vo;
	}

	/**
	 * 上传分片
	 */
	public void uploadPart(UploadPartDTO uploadPart) {
		// 上传任务
		UploadTaskEntity task = getOneUploadTask(uploadPart.getFileIdentifier());
		if(null == task){
			//任务删除的时候返回成功
			task = mapper.getByFileIdentifier(uploadPart.getFileIdentifier());
			if(null == task){
				throw new ServiceException("上传任务不存在");
			}else{
				return;
			}
		}

		if(task.isCompleted()){
			throw new ServiceException("任务已完成");
		}

		Multimap<String, String> headers = null;
		if (StringUtils.hasText(task.getFileType())) {
			headers = HashMultimap.create();
			headers.put("Content-Type", task.getFileType());
		}
		try {
			redisTemplate.opsForSet().add(PRE+task.getFileIdentifier(),String.valueOf(uploadPart.getPartNumber()));
			customMinioClient.uploadPartAsync(task.getBucketName(), task.getObjectKey(),uploadPart.getFile().getInputStream(),
				uploadPart.getFile().getSize(),task.getUploadId(), uploadPart.getPartNumber(), headers);
		}catch (Exception e){
			redisTemplate.opsForSet().remove(PRE+task.getFileIdentifier(),String.valueOf(uploadPart.getPartNumber()));
			throw new ServiceException(e.getMessage());
		}
	}

	/**
	 * 合并分片
	 */
	public String completePart(FileInfoDTO dto) {
		// 上传任务
		UploadTaskEntity task = getOneUploadTask(dto.getFileIdentifier());
		if (ObjectUtils.isEmpty(task)) {
			// 不存在上传任务
			throw new ServiceException("上传任务不存在");
		}
		if (task.isCompleted()) {
			return task.getObjectKey();
		}
		try {
			Long num = redisTemplate.opsForSet().size(PRE+task.getFileIdentifier());
			Long chunkNumber = (long)task.getChunkNumber();
			if(!chunkNumber.equals(num)){
				throw new ServiceException("分片未上传完成，不能进行合并");
			}
			asyncService.asyncComplete(task,dto.getBussId());
			redisTemplate.delete(PRE+task.getFileIdentifier());
			return task.getObjectKey();
		} catch (Exception e) {
			log.error("获取全部分片信息失败。", e);
			throw new ServiceException(e.getMessage());
		}
	}

	/**
	 * 取消分片任务
	 * @param fileIdentifier
	 */
	public void cancalTask(String fileIdentifier){
		UploadTaskEntity task = getOneUploadTask(fileIdentifier);
		if (ObjectUtils.isEmpty(task)) {
			// 不存在上传任务
			throw new ServiceException("任务不存在");
		}
		customMinioClient.abortMultipartUploadAsync(task.getBucketName(),task.getObjectKey(),task.getUploadId());
		mapper.deleteById(task.getId());
		redisTemplate.delete(PRE+fileIdentifier);
	}

	/**
	 * 文件普通上传
	 */
	public String upload(MultipartFile file) {
		try(InputStream is = file.getInputStream()) {
			String bucketName = getBucketName(file.getOriginalFilename());
			String objectKey = getObjectKey(file.getSize(),file.getOriginalFilename());
			minioAPI.uploadInputStream(bucketName, objectKey, is, file.getContentType());
			return objectKey;
		} catch (Exception e) {
			log.error("上传失败，", e);
			throw new ServiceException("上传失败，请重新上传");
		}
	}

	//下载文件
	public void download(String fileUrl){
		this.download(fileUrl,null,null);
	}

	/**
	 * 根据分片索引下载
	 * @param fileUrl
	 * @param partIndex
	 */
	public void download(String fileUrl,int partIndex){
		Long offset = ((partIndex-1) * FILE_DOWNLOAD_ZISE);
		download(fileUrl,offset,FILE_DOWNLOAD_ZISE);
	}

	/**
	 * 根据偏移量下载
	 * @param fileUrl
	 * @param offset
	 * @param length
	 */
	public void download(String fileUrl, Long offset, Long length){
		HttpServletResponse response =
			((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();

		InputStream inputStream = null;
		OutputStream out = null;
		try {
			String bucketName = getBucketName(fileUrl);
			if(null ==offset || null == length){//原文件下载
				inputStream = minioAPI.download(bucketName,fileUrl);
			}else{//分片下载
				inputStream = minioAPI.partDownload(bucketName,fileUrl,offset,length);
			}

			//获取下载文本长度，如果是分片下载，计算分片大小（重新计算最后一片分片大小）
			Long totleSize = minioAPI.getFileSize(bucketName,fileUrl);
			if(null == offset){
				length = totleSize;
			}else{
				if(totleSize < offset + length){
					length = totleSize-offset;
				}
			}

			byte[] buf = new byte[1024*1024];
			int len = 0;

			response.reset();
			response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUrl.substring(fileUrl.indexOf(FILE_NAME_PREFIX_SEPARATOR) + 1), "UTF-8"));
			response.setContentType("application/octet-stream");
			response.setCharacterEncoding("UTF-8");
			response.setContentLengthLong(length);
			if(null != offset ){
				response.setHeader("Content-Length-Total",String.valueOf(totleSize));
			}
			out = response.getOutputStream();
			while ((len = inputStream.read(buf)) != -1){
				out.write(buf, 0, len);
			}
			out.flush();
			out.close();
			inputStream.close();
		} catch (Exception e) {
			log.error("文件下载失败,失败原因:{}",e.getMessage());
			response.setHeader("Content-type", "text/html;charset=UTF-8");
			response.setStatus(500);
			String data = "文件下载失败";
			try {
				if(out == null){
					out = response.getOutputStream();
				}
				out.write(data.getBytes("UTF-8"));
			}catch (Exception e1){}
		}finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
				if (out != null) {
					out.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * 根据文件路径获取文件分片数量
	 * @param fileUrl
	 * @return
	 */
	public int getFilePartNums(String fileUrl){
		Long totleSize = getFileSize(fileUrl);
		return (int)Math.ceil((totleSize / (double)FILE_DOWNLOAD_ZISE));
	}

	/**
	 * 根据文件路径获取文件大小
	 * @param fileUrl
	 * @return
	 */
	public Long getFileSize(String fileUrl){
		try {
			String bucketName = getBucketName(fileUrl);
			return minioAPI.getFileSize(bucketName,fileUrl);
		}catch (Exception e){
			throw new ServiceException(e.getMessage());
		}
	}

	/**
	 * 获取文件大小或分片数量
	 * @param url
	 * @param type
	 * @return
	 */
	public Long fileSizeOrPartNumbers(String url,String type){
		if("SIZE".equals(type)){
			return getFileSize(url);
		}else if("PARTNUM".equals(type)){
			return (long)getFilePartNums(url);
		}
		return null;
	}


	/**
	 * 根据MD5值查询分片任务
	 * @param fileIdentifier
	 * @return
	 */
	private UploadTaskEntity getOneUploadTask(String fileIdentifier){
		return mapper.selectOne(
			Wrappers.<UploadTaskEntity>lambdaQuery().eq(UploadTaskEntity::getFileIdentifier, fileIdentifier)
		);
	}

	/**
	 * 获取存储路径
	 * @param fileSize
	 * @param fileName
	 * @return
	 */
	private String getObjectKey(Long fileSize,String fileName) {
		String date = DateTimeUtil.format(LocalDate.now(),"yyyyMMdd");

		StringBuilder objectKey = new StringBuilder(date);
		objectKey.append(SLASH).append(fileSize).append(FILE_NAME_PREFIX_SEPARATOR).append(fileName);
		return objectKey.toString();
	}

	/**
	 * 获取文件存储bucket
	 * @param fileName
	 * @return
	 */
	public String getBucketName(String fileName){
		String sufix = fileName.substring(fileName.lastIndexOf(".")+1);
		return privateBucketNameFileSufix.contains(sufix) ? MinioBucket.MODEL_BUCKET_NAME_PRIVATE : MinioBucket.MODEL_BUCKET_NAME_PUBLIC;
	}

}
